# -*- python -*-
#     Copyright 2015, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Part of "Nuitka", an optimizing Python compiler that is compatible and
#     integrates with CPython, but also works on its own.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#

# The Nuitka scons file. If you have Scons or platform knowledge, please be
# especially invited and contribute improvements.
#
# This file is used to build an executable or shared library. Nuitka needs no
# build process for itself, although it can be compiled using the same method.

import os, subprocess, sys, re, platform

# The directory containing the C++ files generated by Nuitka to be built using
# scons. They are referred to as sources from here on. The ARGUMENTS is what
# Scons provides to us.
source_name = ARGUMENTS["name"]
source_dir = ARGUMENTS["source_dir"]

# The directory containing Nuitka provided C++ files to be built and where it
# should be used.
nuitka_src = ARGUMENTS["nuitka_src"]
static_src = os.path.join(source_dir, "static")

# We tell Scons that it is a variant to be built, so object files don't end up
# inside that directory which need not be writable.
VariantDir(static_src, os.path.join(nuitka_src, "static_src"), 0)

# The name of executable or extension module that we produce.
result_basepath = ARGUMENTS["result_name"]

def getBoolOption(option_name, default):
    """ Small helper for boolean mode flags."""
    value = ARGUMENTS.get(option_name, "True" if default else "False")

    return value.lower() in ("yes", "true", '1')

# The directory to use for cache directory.
cache_mode = getBoolOption("cache_mode", False)

# Module mode: Create a Python extension module, create an executable otherwise.
module_mode = getBoolOption("module_mode", False)

# Debug mode: Less optimizations, debug information in the resulting binary.
debug_mode = getBoolOption("debug_mode", False)

# Python version to target.
python_version = ARGUMENTS["python_version"]

# Python debug mode: reference count checking, assertions in CPython core.
python_debug = getBoolOption("python_debug", False)

# Optimization mode: Optimize as much as currently possible.
optimize_mode = getBoolOption("optimize_mode", True)

# Full compatibility, even where it's stupid, i.e. do not provide information,
# even if available, in order to assert maximum compatibility. Intended to
# control level of compatibility to absurd.
full_compat_mode = getBoolOption("full_compat", False)

# Experimental mode. Do things that are not yet safe to do.
experimental_mode = getBoolOption("experimental", False)

# Tracing mode. Output program progress.
trace_mode = getBoolOption("trace_mode", False)

# LTO mode: Use link time optimizations of g++ compiler if available and known
# good with the compiler in question. The 4.5 one didn't have good enough
# support, the compiled result would not run correctly.
lto_mode = getBoolOption("lto_mode", False)

# Windows target mode: Compile for Windows.
win_target = getBoolOption("win_target", os.name == "nt")

# Windows subsystem mode: Disable console for windows builds.
win_disable_console = getBoolOption("win_disable_console", False)

# Unstriped mode: Do not remove debug symbols.
unstripped_mode = getBoolOption("unstripped_mode", False)

# Clang compiler mode, default on MacOS X and FreeBSD, optional on Linux.
clang_mode = getBoolOption("clang_mode", False)
if sys.platform == "darwin" or "freebsd" in sys.platform:
    clang_mode = True

# MinGW compiler mode, optional and interesting to Windows only.
mingw_mode = getBoolOption("mingw_mode", False)

# Shared library and compiled modules count, determines the need for the
# compiled module loader.
module_count = int(ARGUMENTS["module_count"])

# Frozen modules count, determines the need for the bytecode frozen
# modules loader.
frozen_modules = int(ARGUMENTS.get("frozen_modules", 0))

# Standalone mode
standalone_mode = getBoolOption("standalone_mode", False)

# Show scons mode, output information about Scons operation
show_scons_mode = getBoolOption("show_scons", False)

# Home of Python to be compiled against, used to find include files and
# libraries to link against.
python_prefix = ARGUMENTS["python_prefix"]

# Target arch, uses for compiler choice and quick linking of constants binary
# data.
target_arch = ARGUMENTS["target_arch"]

# Icon for executable (windows-only)
icon_path = ARGUMENTS.get("icon_path", None)

# Forced MSVC version (windows-only)
msvc_version = ARGUMENTS.get("msvc_version", None)

# Amount of jobs to use.
job_count = GetOption("num_jobs")

def getExecutablePath(filename, initial):
    """ Find an execute in either normal PATH, or Scons detected PATH. """

    # Variable substitution from environment is needed, because this can contain
    # "$CC" which should be looked up too.
    while filename.startswith('$'):
        filename = env[filename[1:]]

    # Append ".exe" suffix  on Windows if not already present.
    if os.name == "nt" and not filename.lower().endswith(".exe"):
        filename += ".exe"

    # Search in "PATH" environment for a file named like it.
    if os.name == "nt":
        separator = ';'
    else:
        separator = ':'

    # Either look at the initial "PATH" as given from the outside or look at the
    # current environment.
    if initial:
        search_path = os.environ["PATH"]
    else:
        search_path = env._dict["ENV"]["PATH"]

    # Now check in each path element, much like the shell will.
    path_elements = search_path.split(separator)

    for path_element in path_elements:
        full = os.path.join(path_element, filename)

        if os.path.exists(full):
            return full
    else:
        return None


def createEnvironment( compiler_tools ):
    args = {}

    # If we are on Windows, and MinGW is not enforced, lets see if we can
    # find "cl.exe", and if we do, disable automatic scan.
    if os.name == "nt" and \
       not mingw_mode and \
       (getExecutablePath("cl", initial = True) is not None or \
        getExecutablePath("gcc", initial = True) is not None):

        args["MSVC_USE_SCRIPT"] = False

    return Environment(
        # We want the outside environment to be passed through.
        ENV          = os.environ,

        # Extra tools configuration for scons.
        tools        = compiler_tools,

        # The shared libraries should not be named "lib...", because CPython
        # requires the filename "module_name.so" to load it.
        SHLIBPREFIX  = "",

        # Under windows, specify the target architecture is needed for Scons
        # to pick up MSVC.
        TARGET_ARCH  = target_arch,

        MSVC_VERSION = msvc_version,

        **args
    )

if mingw_mode:
    # Force usage of MinGW.
    compiler_tools = ["mingw"]
else:
    # Everything else should use default.
    compiler_tools = ["default"]

# Create Scons environment, the main control tool. Don't include "mingw" on
# Windows immediately, we will default to MSVC if available.
env = createEnvironment(
    compiler_tools = compiler_tools
)

# On Windows, in case MSVC was not found and not previously forced, retry with
# it and use that instead then as a fallback. Using both tools in one call
# seems to not work in terms of fallback when both exist.
if os.name == "nt" and \
   compiler_tools == ["default"] and \
   getExecutablePath(env["CXX"], initial = False) is None:
    env = createEnvironment(
        compiler_tools = ["mingw"]
    )

if show_scons_mode:
    print "Initial CXX:", env.get("CXX", None)
    print "Initial CXXVERSION:", env.get("CXXVERSION", None)

if clang_mode:
    # If requested by the user, use the clang compiler, overriding what was
    # said in environment.
    env["CXX"] = "clang"
    env["CXXVERSION"] = None
elif "CXX" in os.environ:
    # If the environment variable CXX is set, use that.
    env["CXX"] = os.environ["CXX"]
    env["CXXVERSION"] = None

# To work around Windows not supporting command lines of greater than 10K by
# default:
def setupSpawn(env):
    def spawn(sh, escape, cmd, args, env):
        # The "del" appears to not work, but is used with large amounts of
        # files to link. So, lets do this ourselves, plus it avoids a process
        # spawn.
        if cmd == "del":
            assert len(args) == 2

            os.unlink(args[1])
            return 0

        # For quoted arguments that end in a backslash, things don't work well
        # this is a workaround for it.
        def removeTrailingSlashQuote(arg):
            if arg.endswith(r'\"'):
                return arg[:-1] + '\\"'
            else:
                return arg

        newargs = ' '.join(
            removeTrailingSlashQuote(arg)
            for arg in
            args[1:]
        )
        cmdline = cmd + ' ' + newargs
        startupinfo = subprocess.STARTUPINFO()

        # Remove useless quoted include directories to "windres", which does not
        # handle them properly in its command line parsing, while they are not
        # used at all.
        if cmd == "windres":
            cmdline = re.sub('--include-dir ".*?"', "", cmdline)

        # CPython2.6 compatibility
        try:
            from subprocess import STARTF_USESHOWWINDOW
        except ImportError:
            from _subprocess import STARTF_USESHOWWINDOW

        startupinfo.dwFlags |= STARTF_USESHOWWINDOW

        proc = subprocess.Popen(
            cmdline,
            stdin       = subprocess.PIPE,
            stdout      = subprocess.PIPE,
            stderr      = subprocess.PIPE,
            startupinfo = startupinfo,
            shell       = False,
            env         = env
        )

        data, err = proc.communicate()
        rv = proc.wait()

        if cmd == "cl":
            data = data[data.find("\r\n") + 2 :]

            source_basenames = [
                os.path.basename(source_file)
                for source_file in
                source_files
            ]

            def check(line):
                return line in ("",  "Generating Code...") or \
                       line in source_basenames

            data = "\r\n".join(
                line
                for line in
                data.split("\r\n")
                if not check(line)
            )

        elif cmd == "rc":
            data = data[data.find("reserved.\r") + 13:]

            data = '\n'.join(
                line
                for line in
                data.split('\n')
                if not "identifier truncated to"
            )
        elif cmd == "link" and module_mode:
            data = "\r\n".join(
                line
                for line in
                data.split("\r\n")
                if "   Creating library" not in line
                # On localized compilers, the message to ignore is not as clear.
                if not (module_mode and ".exp" in line)
            )

        if data.rstrip():
            if not show_scons_mode:
                print cmdline
            print data,

        if err:
            print err,

        return rv

    env["SPAWN"] = spawn

def getGccVersion():
    # Update CXXVERSION in env, after we changed it.
    import SCons

    pipe = SCons.Action._subproc(
        env,
        [env["CXX"], "--version"],
        stdin  = "devnull",
        stderr = "devnull",
        stdout = subprocess.PIPE
    )

    line = pipe.stdout.readline()

    match = re.search(r'[0-9]+(\.[0-9]+){2}', line)

    if match:
        return match.group(0)
    else:
        return None

def getClangVersion():
    import SCons

    pipe = SCons.Action._subproc(
        env, [env["CXX"], "--version"],
        stdin  = "devnull",
        stderr = "devnull",
        stdout = subprocess.PIPE
    )

    line = pipe.stdout.readline()

    match = re.search(r'LLVM ([0-9]+(\.[0-9]+){1})', line)

    if match:
        return match.group(1)

    match = re.search(r'([0-9]+(\.[0-9]+){1})', line)

    if match:
        return match.group(1)
    else:
        return None

# Some versions of Scons have "CXX" not set or None, make it explicit.
if env.get("CXX", None) is None:
    env["CXX"] = None

orig_cxx = env["CXX"]
orig_cxx_version = env.get("CXXVERSION", None)

# Remove "g++" as the compiler, in case it's not really existing or not high
# enough version.
if "g++" in (env["CXX"] or "") and "clang" not in (env["CXX"] or ""):
    gpp_version = getGccVersion()

    if gpp_version is None or int(gpp_version.replace('.', "")) < 440:
        env["CXX"] = None

# Detect compiler to be used from supported ones, if there is no usable g++
# link.
if os.name != "nt" and env["CXX"] is None:
    # Try out the Debian names, as no "gcc" default compiler package needs to
    # be installed.

    for candidate in ("g++-4.9", "g++-4.8", "g++-4.7", "g++-4.6", "g++-4.5",
                      "g++-4.4", "g++44", "clang", "eg++"):

        for system_path in ("/usr/bin", "/usr/local/bin"):
            if os.path.exists(os.path.join(system_path, candidate)):
                env["CXX"] = candidate
                env["CC"] = candidate.replace("++", "cc")

                if "clang" not in candidate:
                    # Update CXXVERSION in env, after we changed it.
                    env["CXXVERSION"] = getGccVersion()

                break

        if env["CXX"] is not None:
            break

    if env["CXX"] is None:
        sys.exit("""\
The g++ compiler '%s' (version %s) doesn't have the sufficient \
version (>= 4.4).""" % (orig_cxx, orig_cxx_version))

if env["CXX"] is None or \
   getExecutablePath(env["CXX"], initial = False) is None:
    if os.name == "nt":
        sys.exit("""\
Error, cannot locate suitable C++ compiler. You have the following options:

a) If a suitable Visual Studio version is installed, it will not be located,
   automatically unless you install pywin32 for the Python installation
   below "%s".

b) To make it find Visual Studio execute from Start Menu the 'Visual Studio
   Command Prompt' or "vcvarsall.bat". That will add Visual Studio to the
   PATH. And it will be detected.

c) Install MinGW to 'C:\\MinGW' then it is automatically picked up. But be
   sure to pick the proper variant (32/64 bits, your Python arch is '%s'). If
   you have it installed, using the matching Python will help.
""" % (sys.exec_prefix, target_arch))
    else:
        sys.exit("Error, cannot locate suitable C++ compiler.")

gcc_mode = "g++" in env["CXX"] or "clang" in env["CXX"]
msvc_mode = win_target and not gcc_mode
mingw_mode = win_target and gcc_mode

if msvc_mode:
    def getMsvcVersionString():
        import SCons.Tool.MSCommon.vc
        return SCons.Tool.MSCommon.vc.get_default_version(env)

    def getMsvcVersion():
        value = getMsvcVersionString()

        value = value.replace("exp","")
        return float(value)


if show_scons_mode:
    print "Scons compiler: Using",
    print getExecutablePath(env["CXX"], initial = False),

    if os.name == "nt" and msvc_mode:
        print "(MSVC %s)" % getMsvcVersionString()

    print

if os.name == "nt":
    setupSpawn(env)

env["BUILD_DIR"] = source_dir

# Store the file signatures database with the rest of the source files
sconsign_dir = os.path.abspath(os.path.join(source_dir, ".sconsign"))

if not os.path.exists(sconsign_dir):
    os.makedirs(sconsign_dir)

env.SConsignFile(sconsign_dir)

# Support for clang.
if "clang" in env["CXX"]:
    env.Append(
        CCFLAGS = ["-w"]
    )
    env.Append(
        CPPDEFINES = ["_XOPEN_SOURCE"]
    )
    env.Append(
        LINKFLAGS = ["-lstdc++" ]
    )

    # Don't export anything by default, this should create smaller executables.
    env.Append(
        CCFLAGS = [
            "-fvisibility=hidden",
            "-fvisibility-inlines-hidden"
        ]
    )

    if debug_mode and not clang_mode:
        env.Append(
            CCFLAGS = ["-Wunused-but-set-variable"]
        )

    clang_version = getClangVersion()
    clang_version = int(clang_version.replace('.', "") + '0')

if gcc_mode:
    if not win_target:
        env.Append(
            CCFLAGS = ["-fvisibility=hidden"]
        )
        env.Append(
            CXXFLAGS = ["-fvisibility-inlines-hidden"]
        )

# Support details for real g++, not clang++.
if "g++" in env["CXX"] and "clang" not in env["CXX"]:
    # Don't export anything by default, this should create smaller executables.

    if env.get("CXXVERSION", None) is None or \
       env.get("CXXVERSION").count('.') < 2:
        env["CXXVERSION"] = getGccVersion()

    # Version dependent options.
    gpp_version = int(env["CXXVERSION"].replace('.', ""))

    # Enforce the minimum version, selecting a potentially existing g++-4.5
    # binary if it's not high enough. This is esp. useful under Debian which
    # allows all compiler to exist next to each other and where g++ might not be
    # good enough, but g++-4.5 would be.
    if gpp_version < 440:
        sys.exit(
            """\
The g++ compiler %s (version %s) doesn't have the sufficient \
version (>= 4.4).""" % (env["CXX"], env["CXXVERSION"])
        )

    # Older g++ complains about aliasing with Py_True and Py_False, but we don't
    # care.
    if gpp_version < 450:
        env.Append(
            CCFLAGS = ["-fno-strict-aliasing"]
        )

    # For LTO mode, the version requirement is even higher, so try that too.
    if lto_mode and gpp_version < 460:
        sys.exit("""\
The g++ compiler %s (version %s) doesn't have the sufficient \
version for lto mode (>= 4.6).""" % (env[ "CXX" ], env[ "CXXVERSION" ]))

    # For g++ 4.6 there are some new interesting functions.
    if gpp_version >= 460:
        env.Append(
            CCFLAGS = ["-fpartial-inlining"]
        )

        if debug_mode:
            env.Append(
                CCFLAGS = ["-Wunused-but-set-variable"]
            )

    # Use link time optimizations so that gcc can help with optimization across
    # files, but unfortunately at this time it seriously slows down the compiled
    # code. This may be because it needs -O3 option to be effective.

    if gpp_version >= 460 and lto_mode:
        env.Append(CCFLAGS = ["-flto"])
        env.Append(LINKFLAGS = ["-flto=%d" % job_count])

        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=noreturn" ] )
        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=pure" ] )
        # env.Append( LINKFLAGS = [ "-Wsuggest-attribute=const" ] )
        # env.Append( CCFLAGS = [ "-Wnoexcept" ] )

        if debug_mode:
            env.Append(LINKFLAGS = ["-O2"])

        if optimize_mode:
            env.Append(
                LINKFLAGS = [
                    "-O3",
                    "-fpartial-inlining",
                    "-freorder-functions",
                ]
            )

    # Give a warning if LTO mode was specified, but won't be used.
    if lto_mode and gpp_version < 460:
        print >> sys.stderr, "Warning, LTO mode specified, but not available."

    # The var-tracking does not scale, disable it. Should we really need it, we
    # can enable it. TODO: Does this cause a performance loss?
    env.Append(CCFLAGS = ["-fno-var-tracking"])

if msvc_mode:
    env.Append(CCFLAGS = ["/EHsc", "/J", "/Gd"])
    env.Append(LINKFLAGS = ["/INCREMENTAL:NO"])

if debug_mode:

    if gcc_mode:
        # Allow g++/clang to point out all kinds of inconsistency to us by
        # raising an error.
        env.Append(
            CCFLAGS = [
                "-Wall",
                "-Werror",
                # Unfortunately Py_INCREF(Py_False) triggers aliasing warnings,
                # which are unfounded, so disable them.
                "-Wno-error=strict-aliasing",
                "-Wno-strict-aliasing",
                # At least for self-compiled Python3.2, and MinGW this happens
                # and has little use anyway.
                "-Wno-error=format",
                "-Wno-format"

            ]
        )
    elif msvc_mode:
        # Disable warnings that system headers already show.
        env.Append(
            CCFLAGS = [
                "/W4", "/wd4505", "/wd4127", "/wd4100", "/wd4702", "/wd4189",
                "/wd4211", "/WX"
            ]
        )

        # Disable warnings, that CPython headers already show.
        if python_version >= "3.4":
            env.Append(
                CCFLAGS = [
                    "/wd4512", "/wd4510", "/wd4610"
                ]
            )



    # As for sequence points, we are abusing it, so we have to allow it.
    if "g++" in env["CXX"]:
        env.Append(
            CCFLAGS = ["-Wno-sequence-point"]
        )

if full_compat_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_FULL_COMPAT"]
    )

if experimental_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_EXPERIMENTAL"]
    )

if trace_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_TRACE"]
    )

if standalone_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_STANDALONE"]
    )

    if "linux" in sys.platform:
        env.Append(
            LIBS = ["dl"]
        )

if win_target:
    # For MinGW and cross compilation, we need to tell the subsystem
    # to target as well as to automatically import everything used.
    if gcc_mode:
        env.Append(
            LINKFLAGS = ["-Wl,--enable-auto-import"]
        )

        if win_disable_console:
            env.Append(
                LINKFLAGS = ["-Wl,--subsystem,windows"]
            )

    # This is used for "PathRemoveFileSpec" used in Windows code.
    env.Append(
        LIBS = ["Shlwapi"]
    )

    if win_disable_console:
        env.Append(
            CPPDEFINES = ["_NUITKA_WINMAIN_ENTRY_POINT"]
        )

if python_debug:
    env.Append(
        CPPDEFINES = ["Py_DEBUG"]
    )

def detectHostMultiarch():
    import commands

    for line in commands.getoutput("dpkg-architecture").split('\n'):
        if line.startswith("DEB_HOST_MULTIARCH="):
            return line.split('=', 1)[1]
    else:
        return None

if gcc_mode and "linux" in sys.platform:
    if python_version.startswith("3.3"):
        host_multiarch = detectHostMultiarch()

        if host_multiarch is not None:
            env.Append(
                CCFLAGS = [
                    "-I" + os.path.join(
                        "/usr/include/",
                        host_multiarch,
                        "python" + python_version
                    )
                ]
            )


# For Nuitka, it generally is OK to break out of the virtualenv, and use the
# original install. Mind you, this is not about executing anything, this is
# about building, and finding the headers to compile against that Python, we
# do not care about any site packages, and so on.

if os.name != "nt" and os.path.islink(os.path.join(python_prefix, ".Python")):
    # Some virtualenv, at least on MacOS, have such handy links.
    python_prefix = os.path.normpath(
        os.path.join(
            os.readlink(os.path.join(python_prefix, ".Python")),
            ".."
        )
    )
elif os.name != "nt" and python_version >= "3.3" and \
     os.path.exists(os.path.join(python_prefix, "bin/activate")):
    # For "venv" of Python3.3, it seems necessary to find include files
    # and libraries on the outside.
    python_binary = os.path.join(python_prefix, "bin", "python")
    python_binary = os.path.realpath(python_binary)

    python_prefix = os.path.normpath(
        os.path.join(
            python_binary,
            "..",
            ".."
        )
    )
elif os.path.exists(os.path.join(python_prefix, "Lib/orig-prefix.txt")):
    python_prefix = open(
        os.path.join(python_prefix, "Lib/orig-prefix.txt")
    ).read()

    # Trailing spaces in the python prefix, please not.
    assert python_prefix == python_prefix.strip()

if os.name == "nt":
    # On Windows, the installation layout is relatively fixed.
    python_header_path = os.path.join(python_prefix, "include")
else:
    # The python header path is a combination of python version and debug
    # indication, make sure the headers are found by adding it to the C++
    # include path.

    python_header_path = os.path.join(
        python_prefix,
        "include",
        "python" + python_version
    )

if not os.path.exists(os.path.join(python_header_path, "Python.h")):
    # Not, for --python-debug other headers are used than for normal
    # compilation.
    sys.exit(
        """\
Error, no 'Python.h' %s headers can be found at '%s', dependency \
not satisfied!""" % (
            "debug" if python_debug else "development",
            python_header_path
        )
    )

env.Append(CPPPATH = [python_header_path])

if win_target:
    # MinGW for 64 bits needs this due to CPython being compiled with MSVC.
    win_lib_path = os.path.join(python_prefix, "libs")

    if os.name == "nt" and target_arch == "x86_64" and gcc_mode:
        new_win_lib_path = os.path.join(source_dir, "libs")
        if not os.path.exists(new_win_lib_path):
            os.makedirs(new_win_lib_path)

        import commands, shutil
        command = r"gendef - %s\SysWOW64\python%s.dll" % (
                os.environ["WINDIR"],
                python_version.replace('.', "")
            )

        defs_filename = os.path.join(
            new_win_lib_path,
            "python%s.defs" % python_version.replace('.', "")
        )
        defs_file = open(
            defs_filename,
            'w'
        )

        startupinfo = subprocess.STARTUPINFO()

        # CPython2.6 compatibility
        try:
            from subprocess import STARTF_USESHOWWINDOW
        except ImportError:
            from _subprocess import STARTF_USESHOWWINDOW

        startupinfo.dwFlags |= STARTF_USESHOWWINDOW

        proc = subprocess.Popen(
            command,
            stdin       = subprocess.PIPE,
            stdout      = defs_file,
            stderr      = subprocess.PIPE,
            startupinfo = startupinfo,
            shell       = False
        )

        data, err = proc.communicate()
        rv = proc.wait()

        assert rv == 0

        defs_file.close()
        defs_file = open(
            defs_filename,
            'r'
        )
        defs = defs_file.read()
        defs_file.close()
        defs_file = open(
            defs_filename,
            'w'
        )
        defs_file.write(defs.replace("Py_InitModule4", "Py_InitModule4_64"))
        defs_file.close()

        shutil.copy(
            r"%s\SysWOW64\python%s.dll" % (
                os.environ["WINDIR"],
                python_version.replace('.', "")
            ),
            new_win_lib_path
        )

        lib_filename = os.path.join(
            new_win_lib_path,
            "libpython%s.a" % python_version.replace('.', "")
        )

        dll_tool = os.path.join(
            os.path.dirname(
                getExecutablePath(env["CXX"], initial = False)
            ),
            "dlltool"
        )

        result = subprocess.call(
            (
                dll_tool,
                "-d",
                defs_filename,
                "-l",
                lib_filename
            ),
            stdout = subprocess.PIPE
        )

        if result != 0:
            sys.exit("Error, call to 'dlltool.exe' failed.")

        win_lib_path = new_win_lib_path

    env.Append(LIBPATH = [win_lib_path])
    env.Append(LIBS = ["python" + python_version.replace('.', "")])
else:
    # Debian and Ubuntu distinguish the system libraries like this.
    if python_debug and \
       python_prefix == "/usr" and \
       not python_version.startswith('3') and \
       platform.dist()[0].lower() in ("debian", "ubuntu"):
        env.Append(LIBS = ["python" + python_version + "_d"])
    else:
        env.Append(LIBS = ["python" + python_version])

    if python_prefix != "/usr" and "linux" in sys.platform:
        env.Append(
            LIBS = ["dl", "pthread", "util", 'm']
        )

        if gcc_mode and "clang" not in env["CXX"]:
            env.Append(
                LINKFLAGS = ["-export-dynamic"]
            )

    # Add the python library path to the library path
    python_lib_path = os.path.join(python_prefix, "lib")
    env.Append(LIBPATH = [python_lib_path])

    # For NetBSD the rpath is required, on FreeBSD it's warned as unused.
    if "netbsd" in sys.platform:
        env.Append(
            LINKFLAGS = ["-rpath=" + python_lib_path]
        )

# The static include files reside in Nuitka installation, which may be where
# the "nuitka.build" package lives.
nuitka_include = os.path.join(
    nuitka_src,
    "include"
)

# We have include files in the build directory and the static include directory
# that is located inside Nuitka installation.
env.Append(
    CPPPATH = [
        source_dir,
        nuitka_include
    ]
)

if debug_mode or unstripped_mode:
    # Use debug format, so we get good tracebacks from it.
    if gcc_mode:
        env.Append(
            CCFLAGS = ["-g"]
        )
        env.Append(
            ASFLAGS = ["-g"]
        )

        if "g++" in env["CXX"]:
            env.Append(
                CCFLAGS = ["-feliminate-unused-debug-types"]
            )
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/Z7"]
        )

        # Higher MSVC versions need this for parallel compilation
        if job_count > 1 and getMsvcVersion() >= 11:
            env.Append(
                CCFLAGS = ["/FS"]
            )

        env.Append(
            LINKFLAGS = ["/DEBUG"]
        )
else:
    if gcc_mode:
        if sys.platform == "darwin":
            env.Append(
                LINKFLAGS = ["-Wno-deprecated-declarations"]
            )
        else:
            env.Append(
                LINKFLAGS = ["-s"]
            )

# When debugging, optimize less than when optimizing, when not remove
# assertions.
if debug_mode:
    if not optimize_mode:
        if gcc_mode or msvc_mode:
            env.Append(
                CCFLAGS = ["-O2"]
            )
else:
    env.Append(
        CPPDEFINES = ["__NUITKA_NO_ASSERT__"]
    )

if optimize_mode:
    if gcc_mode:
        env.Append(
            CCFLAGS = ["-O3"]
        )

        # Check inlining of calls, except in debug mode, where it will all be
        # wrong due to additional code.
        # if not debug_mode:
        #     env.Append( CCFLAGS = [ "-Winline" ] )
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/Ox"]
        )

# MinGW for 64 bits needs this due to CPython bugs.
if os.name == "nt" and target_arch == "x86_64" and gcc_mode:
    env.Append(CPPDEFINES = ["MS_WIN64"])

# Set load libpython from binary directory default
if standalone_mode and gcc_mode and sys.platform != "darwin":
    env.Append(
        LINKFLAGS = ["-Wl,-R,'$$ORIGIN'"])

def getLinkerArch():
    if "linux" in sys.platform:
        if target_arch == "x86_64":
            return "elf64-x86-64"
        elif target_arch == "armv5tel":
            # Debian arch "armel"
            return "elf32-littlearm"
        elif target_arch == "armv7l":
            # Debian arch "armhf"
            return "elf32-littlearm"
        else:
            # TODO: Detect from objdump mayhaps.
            return None
    else:
        # TODO: Missing for MacOS, FreeBSD, other Linux
        return None

constants_bin_filename = os.path.join(source_dir,"__constants.bin")

if win_target and not module_mode:
    # On Windows constants are accesses as a resource, except in shared
    # libraries, where that option is not available.
    constants_generated_filename = None
elif gcc_mode and getLinkerArch() is not None:
    env.Append(
        LINKFLAGS = [
            "-Wl,-b", "-Wl,binary",
            "-Wl,%s" % constants_bin_filename,
            "-Wl,-b", "-Wl,%s" % getLinkerArch(),
            "-Wl,-defsym",
            "-Wl,%sconstant_bin=_binary_%s___constants_bin_start" % (
                '_' if mingw_mode else "",
                "".join(re.sub("[^a-zA-Z0-9_]",'_',c) for c in source_dir)
            )
        ]
    )

    constants_generated_filename = None
else:
    constants_generated_filename = os.path.join(
        source_dir,
        "__constants_data.c"
    )

    with open(constants_generated_filename, 'w') as output:
        output.write("const unsigned char constant_bin[] =\n{\n")

        for count, stream_byte in enumerate(
                open(constants_bin_filename, "rb").read()
            ):
            if count % 16 == 0:
                if count > 0:
                    output.write('\n')

                output.write("   ")

            output.write(" 0x%02x," % ord(stream_byte))

        output.write("\n};\n");

env.Append(
    CPPDEFINES = ["_NUITKA_FROZEN=%d" % frozen_modules]
)

# Tell compiler to create a shared library or program.
if module_mode:
    if "g++" in env["CXX"]:
        env.Append(
            CCFLAGS = ["-shared"]
        )
    elif "clang" in env["CXX"]:
        pass
    elif msvc_mode:
        env.Append(
            CCFLAGS = ["/LD"]
        )
    else:
        assert False
else:
    if msvc_mode:
        env.Append(
            CCFLAGS = ["/MT"]
        )

if module_mode:
    env.Append(
        CPPDEFINES = ["_NUITKA_MODULE"]
    )
else:
    env.Append(
        CPPDEFINES = ["_NUITKA_EXE"]
    )

def discoverSourceFiles():
    result = []

    def getStatic(sub_path):
       return os.path.join(static_src, sub_path.replace('/', os.path.sep))

    result.append(getStatic("CompiledFunctionType.cpp"))
    result.append(getStatic("CompiledGeneratorType.cpp"))
    result.append(getStatic("CompiledMethodType.cpp"))
    result.append(getStatic("CompiledFrameType.cpp"))
    result.append(getStatic("CompiledCodeHelpers.cpp"))
    result.append(getStatic("InspectPatcher.cpp"))

    if win_target:
        result.append(getStatic("win32_ucontext_src/fibers_win32.cpp"))
    elif "openbsd" in sys.platform:
        result.append(getStatic("libcoro_ucontext_src/fibers_coro.cpp"))
        result.append(getStatic("libcoro_ucontext_src/coro.c"))

        env.Append(
            CPPDEFINES = ["CORO_SJLJ"]
        )
    elif target_arch == "x86_64" and "linux" in sys.platform:
        result.append(getStatic("x64_ucontext_src/fibers_x64.cpp"))
        result.append(getStatic("x64_ucontext_src/swapfiber.S"))
    elif target_arch == "armv5tel":
        result.append(getStatic("arm_ucontext_src/fibers_arm.cpp"))
        result.append(getStatic("arm_ucontext_src/ucontext.cpp"))
        result.append(getStatic("arm_ucontext_src/getcontext.asm"))
    else:
        # Variant based on getcontext/setcontext/swapcontext/makecontext
        result.append(getStatic("gen_ucontext_src/fibers_gen.cpp"))

    for filename in os.listdir(source_dir):
        if filename.endswith(".cpp"):
            result.append(os.path.join(source_dir, filename))

    # If more than one module is included, we need the unfreezer.
    if module_count > 1:
        result.append(getStatic("MetaPathBasedLoader.cpp"))

    if constants_generated_filename is not None:
        result.append(constants_generated_filename)

    return result

source_targets = []

# Prepare the use of a custom specs file for windows targets. We change the used
# specs for linking to avoid the use of the wrong (for CPython) run time
# library.
if win_target:
    rc_content = []

    # Which files to depend on.
    rc_file_dependencies = []

    if not module_mode:

        rc_content.append(
            '3 RCDATA "%s"' % constants_bin_filename.replace('\\', '/')
        )

        rc_file_dependencies.append(constants_bin_filename)

        if python_version < "3.3":
            manifest_filename = os.path.join(
                source_dir,
                "resources.manifest"
            )

            # Find the "python.exe" in a normal install, this is assuming we
            # broke out of the virtualenv already.
            python_exe = os.path.join(
                 python_prefix,
                 "python.exe"
            )

            assert os.path.exists(python_exe), python_exe

            if msvc_mode:
                result = subprocess.call(
                    (
                        env["MT"],
                        "-inputresource:%s;#1" % python_exe,
                        "-out:%s" % manifest_filename
                    ),
                    stdout = subprocess.PIPE
                )

                if result != 0:
                    sys.exit("Error, call to 'mt.exe' to extract manifest failed.")
            else:
                result = subprocess.call(
                    (
                        env["RC"],
                        "--input", python_exe,
                        "--output", manifest_filename,
                        "--output-format", "rc"
                    ),
                    stdout = subprocess.PIPE
                )

                if result != 0:
                    sys.exit("Error, call to 'windres.exe' to extract manifest failed.")

                # Need to reduce to what we use, and remove the quoting of the
                # actual manifest. We created a full "rc" file, but that we do
                # not have use for.
                manifest_file = open(manifest_filename, 'r')
                inside = False
                contents = []
                for line in manifest_file:
                    if "RT_MANIFEST" in line:
                        inside = True
                        continue

                    if not inside:
                        continue

                    line = line.replace(r"\r\n", "\r\n")
                    line = line.replace('""', '"')

                    contents.append(
                        line[line.find('"')+1:line.rfind('"')]
                    )

                manifest_file = open(manifest_filename, 'w')
                manifest_file.write("".join(contents[1:-1]))
                manifest_file.close()

            rc_content.append(
                "1 RT_MANIFEST resources.manifest"
            )

            rc_file_dependencies.append(manifest_filename)

        if icon_path:
            rc_content.append(
                '2 ICON MOVEABLE PURE LOADONCALL DISCARDABLE "%s"' % (
                    icon_path.replace('\\', '/')
                )
            )

            rc_file_dependencies.append(icon_path)

    rc_filename = os.path.join(source_dir, "resources.rc")

    rc_file = open(rc_filename, 'w')

    rc_content.insert(
        0,
        '#include "winuser.h"'
    )
    rc_file.write('\n'.join(rc_content))
    rc_file.close()

    res_target = env.RES(rc_filename)

    for rc_file_dependency in rc_file_dependencies:
        Depends(res_target, rc_file_dependency)

    source_targets.append(
        res_target
    )

source_files = discoverSourceFiles()

if module_mode:
    # For Python modules, the standard shared library extension is not what
    # gets used.
    if win_target:
        module_suffix = ".pyd"
    else:
        module_suffix = ".so"

    env["SHLIBSUFFIX"] = module_suffix

    target = env.SharedLibrary(
        result_basepath,
        source_files  + source_targets
    )
else:
    # Avoid dependency on MinGW libraries.
    if win_target and gcc_mode:
        env.Append(
            LINKFLAGS = [
                "-static-libgcc",
                "-static-libstdc++"
            ]
        )

    target = env.Program(
        result_basepath + ".exe",
        source_files + source_targets
    )

# On some architectures, makecontext cannot pass pointers reliably.
if target_arch == "x86_64" and "linux" in sys.platform:
    env.Append(CPPDEFINES = ["_NUITKA_MAKECONTEXT_INTS"])

# Avoid IO for compilation as much as possible, this should make the
# compilation more memory hungry, but also faster.
if gcc_mode:
    env.Append(CCFLAGS = "-pipe")

if "CPPFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CPPFLAGS"].split())
if "CCFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CCFLAGS"].split())
if "CXXFLAGS" in os.environ:
    env.Append(CCFLAGS = os.environ["CXXFLAGS"].split())

if "LDFLAGS" in os.environ:
    env.Append(LINKFLAGS = os.environ["LDFLAGS"].split())

# Remove the target file to avoid cases where it falsely doesn't get rebuild
# and then lingers from previous builds,
if os.path.exists(target[0].abspath):
    os.unlink(target[0].abspath)

if show_scons_mode:
    print "scons: Told to run compilation on %d CPUs." % job_count

# Cached, when done, by the fastest possible algorithm and right inside the
# build directory. Makes no sense of course, if that is removed later on by
# Nuitka.
if cache_mode:
    CacheDir(os.path.join(source_dir, "cache-" + target_arch))
    Decider("MD5-timestamp")

Default(target)
